/*
 * # 应用所有存储过程:
 *  Calendar - 日历
 * 
 * # 描述:
 *  向所有用户显示日历，并允许系统管理人员管理本组织内部使用的节假日和工作日，这是在业务中工作日计数的主要依据。
 * 
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */
 
use "pittypat_app";
go

/*
 * # 存储过程 - Sql Server:
 *   Dates - 月份日期列表
 * 
 * # 描述:
 *   查询指定月份所有日期（从 1 号到 31 号）的列表。
 */

if not exists (select * from "sys"."objects" where "object_id" = object_id(N'"waln_calendar"."p_management_dates"') and "type" in (N'P', N'PC'))
	exec('create procedure "waln_calendar"."p_management_dates" as begin set nocount on; end');
go

alter procedure "waln_calendar"."p_management_dates"
	@year smallint, -- 年份 
	@month tinyint, -- 月份 
	@_context nvarchar(4000), -- 当前环境变量参数
	@_success bit out, -- 指示操作是否成功 
	@_message nvarchar(4000) out  -- 操作中产生的消息 
as
begin
	-- 不自动向终端返回受影响行数的计数
	set nocount on;
	
	-- 上一月份所在的年份
	declare @prevMonthYear int;
    -- 上一月份
    declare @prevMonth int;
    -- 下一月份所在的年份
	declare @nextMonthYear int;
    -- 下一月份
    declare @nextMonth int;
    
	-- 默认指示操作失败
	set @_success = 0;
	set @_message = N'';

	-- 验证参数 @year (年份)
	if @year is null 
	begin
		set @_message = N'没有提供 年份。';
		return;
	end

	-- 验证参数 @month (月份)
	if @month is null 
	begin
		set @_message = N'没有提供 月份。';
		return;
	end
    
    -- 上一个月所在年份
    set @prevMonthYear = case when @month = 1 then @year - 1 else @year end;
    -- 上一个月
    set @prevMonth = case when @month = 1 then 12 else @month - 1 end;
    -- 下一个月所在年份
    set @nextMonthYear = case when @month = 12 then @year + 1 else @year end;
    -- 下一个月
    set @nextMonth = case when @month = 12 then 1 else @month + 1 end;
	
	-- 同时返回上一月、指定月和下一月，因为日历列表中通常会包含上一月的最后几天和下一月的开始几天
	select
		v.*
	from
		"waln_calendar"."v_management_date" as v
	where
		(year(v."Value") = @year and month(v."Value") = @month) or
        (year(v."Value") = @prevMonthYear and month(v."Value") = @prevMonth) or
        (year(v."Value") = @nextMonthYear and month(v."Value") = @nextMonth);

	-- 指示操作成功
	set @_success = 1;
end
go

/*
 * # 存储过程 - Sql Server:
 *   SetWorkday - 设置工作日
 * 
 * # 描述:
 *   设置指定的日期，包括是否工作日或节假日，以及节日名称。
 */

if not exists (select * from "sys"."objects" where "object_id" = object_id(N'"waln_calendar"."p_management_setworkday"') and "type" in (N'P', N'PC'))
	exec('create procedure "waln_calendar"."p_management_setworkday" as begin set nocount on; end');
go

alter procedure "waln_calendar"."p_management_setworkday"
	@value date, -- 日期值 
	@isWorkday bit, -- 是否工作日 
	@_context nvarchar(4000), -- 当前环境变量参数
	@_success bit out, -- 指示操作是否成功 
	@_message nvarchar(4000) out  -- 操作中产生的消息 
as
begin
	-- 不自动向终端返回受影响行数的计数
	set nocount on;

	-- 默认指示操作失败
	set @_success = 0;
	set @_message = N'';

	-- 验证参数 @value (日期值)
	if @value is null 
	begin
		set @_message = N'没有提供 日期值。';
		return;
	end

	-- 验证参数 @isWorkday (是否工作日)
	if @isWorkday is null 
	begin
		set @_message = N'没有提供 是否工作日。';
		return;
	end
    
    if exists (select "Value" from "waln_calendar"."dates" where "Value" = @value)
	begin
		update 
			"waln_calendar"."dates" 
		set 
			"IsWorkday" = @isWorkday 
		where 
			"Value" = @value;
	end
    else
	begin
		insert into "waln_calendar"."dates"("Value", "IsWorkday", "Festivals")
        values(@value, @isWorkday, null);
	end

	-- 返回 SetWorkday.date
	select
		v.*
	from
		"waln_calendar"."v_management_date" as v
	where
		v."Value" = @value;

	-- 指示操作成功
	set @_success = 1;
end
go

/*
 * # 存储过程 - Sql Server:
 *   SetFestivals - 设置节日
 * 
 * # 描述:
 *   设置指定日期的节日名称。
 */

if not exists (select * from "sys"."objects" where "object_id" = object_id(N'"waln_calendar"."p_management_setfestivals"') and "type" in (N'P', N'PC'))
	exec('create procedure "waln_calendar"."p_management_setfestivals" as begin set nocount on; end');
go

alter procedure "waln_calendar"."p_management_setfestivals"
	@value date, -- 日期值 
	@festivals nvarchar(50), -- 节日名称 
	@_context nvarchar(4000), -- 当前环境变量参数
	@_success bit out, -- 指示操作是否成功 
	@_message nvarchar(4000) out  -- 操作中产生的消息 
as
begin
	-- 不自动向终端返回受影响行数的计数
	set nocount on;

	-- 默认指示操作失败
	set @_success = 0;
	set @_message = N'';

	-- 验证参数 @value (日期值)
	if @value is null 
	begin
		set @_message = N'没有提供 日期值。';
		return;
	end

    if exists (select "Value" from "waln_calendar"."dates" where "Value" = @value)
	begin
		update 
			"waln_calendar"."dates" 
		set 
			"Festivals" = @festivals 
		where 
			"Value" = @value; 
	end
    else
	begin
		insert into "waln_calendar"."dates"("Value", "IsWorkday", "Festivals")
        values(@value, null, @festivals);
	end

	-- 返回 SetFestivals.date
	select
		v.*
	from
		"waln_calendar"."v_management_date" as v
	where
		v."Value" = @value;

	-- 指示操作成功
	set @_success = 1;
end
go

/*
 * # 存储过程 - Sql Server:
 *   Dates - 月份日期列表
 * 
 * # 描述:
 *   查询指定月份所有日期（从 1 号到 31 号）的列表。
 */

if not exists (select * from "sys"."objects" where "object_id" = object_id(N'"waln_calendar"."p_everyone_dates"') and "type" in (N'P', N'PC'))
	exec('create procedure "waln_calendar"."p_everyone_dates" as begin set nocount on; end');
go

alter procedure "waln_calendar"."p_everyone_dates"
	@year smallint, -- 年份 
	@month tinyint, -- 月份 
	@_context nvarchar(4000), -- 当前环境变量参数
	@_success bit out, -- 指示操作是否成功 
	@_message nvarchar(4000) out  -- 操作中产生的消息 
as
begin
	-- 不自动向终端返回受影响行数的计数
	set nocount on;
	
	-- 上一月份所在的年份
	declare @prevMonthYear int;
    -- 上一月份
    declare @prevMonth int;
    -- 下一月份所在的年份
	declare @nextMonthYear int;
    -- 下一月份
    declare @nextMonth int;
    
	-- 默认指示操作失败
	set @_success = 0;
	set @_message = N'';

	-- 验证参数 @year (年份)
	if @year is null 
	begin
		set @_message = N'没有提供 年份。';
		return;
	end

	-- 验证参数 @month (月份)
	if @month is null 
	begin
		set @_message = N'没有提供 月份。';
		return;
	end

    -- 上一个月所在年份
    set @prevMonthYear = case when @month = 1 then @year - 1 else @year end;
    -- 上一个月
    set @prevMonth = case when @month = 1 then 12 else @month - 1 end;
    -- 下一个月所在年份
    set @nextMonthYear = case when @month = 12 then @year + 1 else @year end;
    -- 下一个月
    set @nextMonth = case when @month = 12 then 1 else @month + 1 end;
    
	-- 同时返回上一月、指定月和下一月，因为日历列表中通常会包含上一月的最后几天和下一月的开始几天
	select
		v.*
	from
		"waln_calendar"."v_everyone_date" as v
	where
		(year(v."Value") = @year and month(v."Value") = @month) or
        (year(v."Value") = @prevMonthYear and month(v."Value") = @prevMonth) or
        (year(v."Value") = @nextMonthYear and month(v."Value") = @nextMonth);

	-- 指示操作成功
	set @_success = 1;
end
go

/*
 * # 存储过程 - Sql Server:
 *   Diff - 计算工作日
 * 
 * # 描述:
 *   计算两个日期之间的工作日数目。
 */

if not exists (select * from "sys"."objects" where "object_id" = object_id(N'"waln_calendar"."p_calc_diff"') and "type" in (N'P', N'PC'))
	exec('create procedure "waln_calendar"."p_calc_diff" as begin set nocount on; end');
go

alter procedure "waln_calendar"."p_calc_diff"
	@from date, -- 起始日期 
	@to date, -- 截至日期 
	@days int out, -- 工作日数目 
	@_context nvarchar(4000), -- 当前环境变量参数
	@_success bit out, -- 指示操作是否成功 
	@_message nvarchar(4000) out  -- 操作中产生的消息 
as
begin
	-- 不自动向终端返回受影响行数的计数
	set nocount on;

	-- 临时的日期值
	declare @d date;
    -- 是否工作日
    declare @isWorkday bit;
    -- 符号：如果起始日期大于截至日期，则返回小于 0 的值
    declare @s int;
	-- 星期
	declare @dayofweek int;
    
	-- 默认指示操作失败
	set @_success = 0;
	set @_message = N'';

	-- 初始化 out 参数 @days (工作日数目)
	set @days = null;

	-- 验证参数 @from (起始日期)
	if @from is null 
	begin
		set @_message = N'没有提供 起始日期。';
		return;
	end

	-- 验证参数 @to (截至日期)
	if @to is null 
	begin
		set @_message = N'没有提供 截至日期。';
		return;
	end

	if abs(datediff("Day", @from, @to)) > 999
	begin
		set @_message = N'两个日期之间不要超过 999 天。';
		return;
	end

	set datefirst 1;
	set @dayofweek = datepart(dw, @from);
    
    set @s = 1;
    if @from = @to 
	begin
		-- 如果起始日期等于截至日期
		if exists (select "Value" from "waln_calendar"."dates" where "Value" = @from and "IsWorkday" = 1)
			set @days = 1;
		else
			-- 检查是否周末，如果不是周末为 1，周末为 0
			set @days = case when @dayofweek < 6 then 1 else 0 end;
	end
	else
	begin
		-- 如果 @from > @to，交换两个值，并设置符号为小于 0 的值
		if @from > @to 
		begin
			set @d = @from;
            set @from = @to;
            set @to = @d;
            set @s = -1;
		end
        
        -- 初始化工作日数目
        set @days = 0;
        -- 循环的中间变量
		set @d = @from;
        
        while @d <= @to
		begin
            -- 重置为 null
            set @isWorkday = null;
            
            -- 检查是否已经设置了工作日指示
            select @isWorkday = "IsWorkday" from "waln_calendar"."dates" where "Value" = @d;
            
            -- 没有设置工作日指示，则判断是否周末
            if @isWorkday is null 
			begin
				set datefirst 1;
				set @isWorkday = case when datepart(dw, @d) < 6 then 1 else 0 end;
            end
            
            -- 如果是工作日，则增加计数
            if @isWorkday = 1 
			begin
				set @days = @days + 1;
            end
			
			-- 下一个日期
            set @d = dateadd("DAY", 1, @d);
        end
    end
    
    -- 符号
    if @s < 0 
	begin
		set @days = @s * @days;
    end

	-- 指示操作成功
	set @_success = 1;
end
go

/*
 * # 存储过程 - Sql Server:
 *   Add - 添加工作日
 * 
 * # 描述:
 *   在指定的日期上添加指定工作日后返回一个新日期。
 */

if not exists (select * from "sys"."objects" where "object_id" = object_id(N'"waln_calendar"."p_calc_add"') and "type" in (N'P', N'PC'))
	exec('create procedure "waln_calendar"."p_calc_add" as begin set nocount on; end');
go

alter procedure "waln_calendar"."p_calc_add"
	@from date, -- 起始日期 
	@days int, -- 工作日数目 
	@to date out, -- 新日期 
	@_context nvarchar(4000), -- 当前环境变量参数
	@_success bit out, -- 指示操作是否成功 
	@_message nvarchar(4000) out  -- 操作中产生的消息 
as
begin
	-- 不自动向终端返回受影响行数的计数
	set nocount on;

	-- 计数
	declare @n int;
    -- 工作日符号，小于 0 为 -1，大于 0 为 1
    declare @s int;
    -- 是否工作日
    declare @isWorkday bit;
    -- 临时日期
    declare @d date;
    
	-- 默认指示操作失败
	set @_success = 0;
	set @_message = N'';

	-- 初始化 out 参数 @to (新日期)
	set @to = null;

	-- 验证参数 @from (起始日期)
	if @from is null 
	begin
		set @_message = N'没有提供 起始日期。';
		return;
	end

	-- 验证参数 @days (工作日数目)
	if @days is null 
	begin
		set @_message = N'没有提供 工作日数目。';
		return;
	end

	if @days > 999 or @days < -999
	begin
		set @_message = N'两个日期之间不要超过 999 天。';
		return;
	end

    if @days = 0 
	begin
		set @to = @from;
        set @_success = 1;
        return;
    end
    
    set @s = case when @days < 0 then -1 else 1 end;
    set @days = abs(@days);

	set @n = 0;
    set @d = @from;
    while 1 = 1
	begin
		-- 查找下一个工作日
		while 1 = 1
		begin
			-- 向前或向后一天
			set @d = dateadd("DAY", @s, @d);
			set @isWorkday = null;
            
			-- 检查是否已经设置了工作日指示
			select @isWorkday = "IsWorkday" from "waln_calendar"."dates" where "Value" = @d;
			
			-- 没有设置工作日指示，则判断是否周末
			if @isWorkday is null 
			begin
				set datefirst 1;
				set @isWorkday = case when datepart(dw, @d) < 6 then 1 else 0 end;
			end

			if @isWorkday = 1 
			begin
				break
			end
        end
        
        set @n = @n + 1;

		if @n = @days 
		begin
			break;
        end
    end
    
    set @to = @d;

	-- 指示操作成功
	set @_success = 1;
end
go
